// Author: johnsiilver@gmail.com(John Doak)

/*
Package schema provides methods for reading and validating JSON data againsts a JSON schema.
*/
package schema 

import (
 "errors"
 "fmt"
 "io"
 "io/ioutil"
 "encoding/json"
 "strings"
 
)

// JsonReader takes a path(terminated by "/") and a file name, reads the
// file, sends it for decoding into a map[string]interface{}, and returns
// the resulting map object.
func JsonReader(path string, name string) (map[string]interface{}, error) {
  filename := path + name
  schemaTxt, err := ioutil.ReadFile(filename)
  if err != nil { return nil, errors.New(fmt.Sprintf("Could not read in schema: %s/%s : %s", path, name, err.Error())) }

  // Decode from JSON.
  return DecodeJson(string(schemaTxt))
}

// DecodeJson turns a JSON string into a map[string]interface{}.
func DecodeJson(data string) (map[string]interface{}, error) {
  var m interface {}
  dec := json.NewDecoder(strings.NewReader(string(data)))
  for {
    // If the err is just an EOF, we are all good.
    if err := dec.Decode(&m); err == io.EOF {
      break
    // Crap, its some other error.
    } else if err != nil {
      return m.(map[string]interface{}), err
    }
  }
  return m.(map[string]interface{}), nil
}

// VerifyJsonSchema walks both a Json schema and Json data (both translated to map[string]interface{})
// and determines if the data meets the schema requirements. Note that the data contains extra fields,
// the verifier won't error. Extra fields are fine.  This only tests that the field types are what
// is expected and that required fields exist.
func VerifyJsonSchema(instance map[string]interface{}, schema map[string]interface{}, ignoreRequired bool) error {
  for k, _ := range schema {
    err := walkAttribute(k, []string{k}, schema[k].(map[string]interface{}), instance, ignoreRequired)
    if err != nil {
      return err
    }
  }
  return nil
}

// walkAttribute follows down a JSON tree structure recursively verifying that the instance of the Schema obeys the Schema.
// key: The key of the schema and instance we are validating.
// keyChain: A list of all keys in the tree prior to this one, so that we can give error messages that tell where in the 
//     tree we had issues.
// schemaLevel: A pointer to the particular entry in the schema we are at.
// instanceLevel: A pointer to the particular entry in the instance we are at.
// ignoreRequired: Occassionally you may have an instance that you want to validate, but you want to ignore the fact
//    that some attributes are required.  If you set this to false, it will validate the schema but not require
//    "required" attributes.
func walkAttribute(key string, keyChain []string, schemaLevel map[string]interface{}, instanceLevel interface{}, ignoreRequired bool) error {
  // Determine if this key is required.
  required := schemaLevel["required"].(bool)

  // This is the keychain up the level above what we are checking.
  // This is used for clearer error output. Aka: if we are checking hardware:model:revision,
  // this would be set to "hardware:model", which is the part of the chain an error would
  // occur in.
  chainError := strings.Join(keyChain[:len(keyChain)-1], ":")
  
  // If the key is required and not present, throw an error.
  if _,present := instanceLevel.(map[string]interface{})[key]; !present && required && ignoreRequired == false {
    return errors.New(fmt.Sprintf("Schema level: %s requires field: %s, which was not found.", chainError, key))
  }

  // The key was not present at this level and is not required, so no need to descend.
  if _,present := instanceLevel.(map[string]interface{})[key]; !present {
    return nil
  }
 
  // Okay, now that we've validated that the key exists, change the instanceLevel down to the next value.  
  instanceLevel = instanceLevel.(map[string]interface{})[key]
  
  // This is the type we expect the value at instanceLevel to be.
  keyType := schemaLevel["type"].(string)

  // Validate that the instanceLevel value is the type we expect according to the schema.
  switch keyType {
    case "string":
      switch instanceLevel.(type) {
        case string:
          break
        default:
          return errors.New(fmt.Sprintf("Schema level: %s requires field: %s is a string.", chainError, key))
      }
    case "boolean":
      switch instanceLevel.(type) {
        case bool:
          break
        default:
          return errors.New(fmt.Sprintf("Schema level: %s requires field: %s is a bool.", chainError, key))
      }
    case "number":
      switch instanceLevel.(type) {
        case int, int32, int64, float32, float64:
         break
        default:
          return errors.New(fmt.Sprintf("Schema level: %s requires field: %s is a number.", chainError, key))
      }
    case "array":
      // Validate the instance we are testing is an array of some data.
      switch instanceLevel.(type) {
        case []interface{}:
          break
        default:
          return errors.New(fmt.Sprintf("Schema level: %s requires field: %s is an array.", chainError, key))
      }
      // Now we need to figure out what the array of data is supposed to be and validate it is indeed that type of data.
      itemDef := schemaLevel["items"].(map[string]interface{})
      switch itemDef["type"].(string) {
        case "string":
          for _, entry := range instanceLevel.([]interface{}) {
            switch entry.(type) {
              case string:
                break
              default:
                return errors.New(fmt.Sprint("Schema level: %s requires list field: %s to be a string.", chainError, key, ))
            }
          }
        case "number":
          for _, entry := range instanceLevel.([]interface{}) {
            switch entry.(type) {
              case int, int32, int64, float32, float64:
                break
              default:
                return errors.New(fmt.Sprint("Schema level: %s requires list field: %s to be a number.", chainError, key, ))
            }
          }
        case "boolean":
          for _, entry := range instanceLevel.([]interface{}) {
            switch entry.(type) {
              case bool:
                break
              default:
                return errors.New(fmt.Sprint("Schema level: %s requires list field: %s to be a boolean.", chainError, key, ))
            }
          }
        case "array":
          for _, entry := range instanceLevel.([]interface{}) {
            switch entry.(type) {
              case []interface{}:
                // TODO(jdoak): We need to go through each entry and verify it as well.
                break
              default:
                return errors.New(fmt.Sprint("Schema level: %s requires list field: %s to be an object.", chainError, key, ))
            }
          }
        case "object":
          for _, entry := range instanceLevel.([]interface{}) {
            switch entry.(type) {
              case map[string]interface{}:
                // TODO(jdoak): We need to go through each object and verify it as well.
                //for _, instanceItem := range instanceLevel.([]interface{}) {
                //  VerifyJsonSchema(instance map[string]interface{}, schema map[string]interface{}, ignoreRequired bool)
                //}
                //walkAttribute(key string, keyChain []string, schemaLevel map[string]interface{}, instanceLevel interface{}, ignoreRequired bool)
              default:
                return errors.New(fmt.Sprint("Schema level: %s requires list field: %s to be an object.", chainError, key, ))
            }
          }
      }
    case "object":
      switch instanceLevel.(type) {
        // Okay, they defined that this value is an object, which means we need to walk all the
        // properties to verify.  So we will recursively call this function for the next level down.
        case map[string]interface{}:
          for belowKey, _ := range schemaLevel["properties"].(map[string]interface{}) { 
            belowSchemaLevel := schemaLevel["properties"].(map[string]interface{})[belowKey].(map[string]interface{})
            belowKeyChain := append(keyChain, belowKey)
            err := walkAttribute(belowKey, belowKeyChain, belowSchemaLevel, instanceLevel, ignoreRequired)
            if err != nil {
              return err
            }
          }
          break
        default:
          return errors.New(fmt.Sprintf("Schema level: %s requires field: %s is an object.", chainError, key))
      }
    default:
      return errors.New(fmt.Sprintf("The schema has an unsupport type at Schema level: %s field: %s.", chainError, key))
  }
  return nil
}
